Skip to content

Closes#95#16

Closed
Tyreviel wants to merge 39 commits intomainfrom
connectionID
Closed

Closes#95#16
Tyreviel wants to merge 39 commits intomainfrom
connectionID

Conversation

@Tyreviel
Copy link

@Tyreviel Tyreviel commented Feb 19, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Full-featured HTTP server with static file serving, request filtering, and security controls
    • Rate limiting per IP, URL redirects, security headers, and request logging
    • Web dashboard and landing page interface
  • Documentation

    • Comprehensive project documentation with architecture guides and configuration details
  • Chores

    • Build system setup and CI/CD pipelines for automated testing and Docker image publishing

fmazmz and others added 30 commits February 6, 2026 08:44
* chore: Update POM to Java 25 and rename artifactId/groupId

* update folder name from example to juv25d

---------

Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
* http parser

* Bunny fixes. (only using input stream to recieve requests)

* Bunny review improvements

* Improved http parser ReadLine helper method to eliminate dependency on mark() and reset(). Implemented handleClient() using socket as a try-with-resources to avoid memory leakage in case of exception thrown by httpparser-methods.

* NumberFormatException fix on line 53 -> 60

* chore: Update POM to Java 25 and rename artifactId/groupId (#11)

* chore: Update POM to Java 25 and rename artifactId/groupId

* update folder name from example to juv25d

---------

Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>

* resolve conflicts

---------

Co-authored-by: Kristina <kristina0x7@gmail.com>
Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
* Add ServerLogging.java as separate class for logging. Implement said class in SocketServer.java to return logging information upon opening socket and user connecting to server.

* Update ServerLogging.java to include a static initializer block and an empty utility class to prevent instantiation.

* Update ServerLogging.java to reference same class in getLogger argument.

* Update ServerLogging.java to check if handler has already been instantiated and allow for log level to be set by args in JVM (default level 'INFO' if no args provided).

* normalize logging statements to be consistent

* remove unused imports

* Update SockerServer.java to properly log server socket errors.

---------

Co-authored-by: Mats Rönnqvist <mats.f.ronnqvist@gmail.com>
Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
* update POM with pitest

* add junit plugin dependency
Rebased 4 commits in this PR.
…#27)

* Fix PiTest by defining argLine and removing invalid Mockito javaagent

* self close argline
…edicated ConnectionHandler. (#28)

* Rename SocketServer to Server
Move HTTP request handling logic to a dedicated ConnectionHandler.

* Convert createSocket to an instance method named start().

* Replace console prints with Logger in ConnectionHandler

* Fixed typo in ConnectionHandler

* refactor Server and ConnectionHandler:
- Inject Logger inside of constructor
- Inject handlerFactory into the Server that handles creation of a new ConnectionHandler on each request
- Remove HttpParser from Server as it is not handling the parsing of a request

* accept ConnectionHandlerFactory and not a specific implementation

* normalize handlerFactory name

---------

Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
…tests. (#34)

* Add testing for ServerLogging.java. Configure ServerLogging.java to improve its testability.

* Update logger_shouldNotAddDuplicateHandlers test to properly test non-inclusion of duplicates.

* Update ServerLogging.java to guard against invalid level string entries in configure method (logger.parse => setLevel).
* http parser

* Bunny fixes. (only using input stream to recieve requests)

* Bunny review improvements

* Improved http parser ReadLine helper method to eliminate dependency on mark() and reset(). Implemented handleClient() using socket as a try-with-resources to avoid memory leakage in case of exception thrown by httpparser-methods.

* NumberFormatException fix on line 53 -> 60

* Added foundation for Filters and Plugins. Added FilterChain to use created filters, and a Pipeline class to handle the workflow (Client → Filters → Plugin → Response → Client). Modified SocketServer handleClient() to use FilterChain. Added example code in App.java for Pipeline usage. TODO: Initialize HttpResponse class

* Fixing build fail

* add init and destroy to filter interface

* add methods to pipeline class

* Add servlet-style filter pipeline with lifecycle support

* add documentation about temporary server impl

* test: verify filters execute in order and plugin is called last

* test: add coverage for filter blocking, lifecycle init, and empty pipeline

* add loggingfiltertest

* fix: construct HttpResponse with required arguments

* fix: construct HttpResponse with required arguments

* Update FilterChainImpl tests to use full HttpResponse constructor to ensure compatibility with future implementations

* remove duplicated class

---------

Co-authored-by: Kristina M <kristina0x7@gmail.com>
Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
… ADR process for the team (#35)

- Add TEMPLATE for writing future ADRs
                                      - Add ADR-001 documenting static file serving architecture

                                      Closes #16
…39)

* feat: make HttpResponse mutable and implement NotFoundPlugin default

- Updated HttpResponse to be mutable to allow filters and plugins to modify responses.
- Implemented NotFoundPlugin as a default fallback for the Pipeline.
- Added null safety check in Pipeline.setPlugin.
- Added unit tests for Pipeline default behavior and NotFoundPlugin.

* ran mvn spotless:apply to fix violations

* Rabbit comment fixes

* Review fixes

* Final buggfixes for this PR (again)
* Implement static file handler with security and tests

Core Implementation:
- Add StaticFileHandler for serving files from /resources/static/
- Add MimeTypeResolver for Content-Type detection
- Add security validation to prevent path traversal attacks

Testing:
- Add MimeTypeResolverTest (15 test cases)
- Add StaticFileHandlerTest (20+ test cases)
- All tests passing

Example Files:
- Add index.html demo page with gradient styling
- Add styles.css for professional styling
- Add app.js for JavaScript functionality demo

Note: Integration with Server/ConnectionHandler will be added
after PR #28 merges to avoid conflicts.

Foundation work for #18

* Introduce ADR structure and first ADR - Add ADR README explaining the ADR process for the team
                                     - Add TEMPLATE for writing future ADRs
                                      - Add ADR-001 documenting static file serving architecture

                                      Closes #16

* Rename SocketServer to Server Move HTTP request handling logic to a dedicated ConnectionHandler. (#28)

* Rename SocketServer to Server
Move HTTP request handling logic to a dedicated ConnectionHandler.

* Convert createSocket to an instance method named start().

* Replace console prints with Logger in ConnectionHandler

* Fixed typo in ConnectionHandler

* refactor Server and ConnectionHandler:
- Inject Logger inside of constructor
- Inject handlerFactory into the Server that handles creation of a new ConnectionHandler on each request
- Remove HttpParser from Server as it is not handling the parsing of a request

* accept ConnectionHandlerFactory and not a specific implementation

* normalize handlerFactory name

---------

Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>

* Add testing for ServerLogging.java. Configure ServerLogging.java for tests. (#34)

* Add testing for ServerLogging.java. Configure ServerLogging.java to improve its testability.

* Update logger_shouldNotAddDuplicateHandlers test to properly test non-inclusion of duplicates.

* Update ServerLogging.java to guard against invalid level string entries in configure method (logger.parse => setLevel).

* feature/FilterPlugin (#17)

* http parser

* Bunny fixes. (only using input stream to recieve requests)

* Bunny review improvements

* Improved http parser ReadLine helper method to eliminate dependency on mark() and reset(). Implemented handleClient() using socket as a try-with-resources to avoid memory leakage in case of exception thrown by httpparser-methods.

* NumberFormatException fix on line 53 -> 60

* Added foundation for Filters and Plugins. Added FilterChain to use created filters, and a Pipeline class to handle the workflow (Client → Filters → Plugin → Response → Client). Modified SocketServer handleClient() to use FilterChain. Added example code in App.java for Pipeline usage. TODO: Initialize HttpResponse class

* Fixing build fail

* add init and destroy to filter interface

* add methods to pipeline class

* Add servlet-style filter pipeline with lifecycle support

* add documentation about temporary server impl

* test: verify filters execute in order and plugin is called last

* test: add coverage for filter blocking, lifecycle init, and empty pipeline

* add loggingfiltertest

* fix: construct HttpResponse with required arguments

* fix: construct HttpResponse with required arguments

* Update FilterChainImpl tests to use full HttpResponse constructor to ensure compatibility with future implementations

* remove duplicated class

---------

Co-authored-by: Kristina M <kristina0x7@gmail.com>
Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>

* Add charset=utf-8 to text-based Content-Type headers

* Integrate StaticFileHandler with Pipeline architecture

* Refactor  to use immutable fields and clean up unused setters; update  with usage notes for future integration.

* Enhance StaticFilesPlugin to properly handle headers with map iteration; simplify and clarify class documentation.

* Update tests to verify Content-Type headers include charset=utf-8

* connect StaticFilesPlugin

---------

Co-authored-by: johanbriger <johanbriger@gmail.com>
Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
Co-authored-by: Mats Rönnqvist <203552386+bamsemats@users.noreply.github.com>
Co-authored-by: Linus Westling <141355850+LinusWestling@users.noreply.github.com>
Co-authored-by: Kristina M <kristina0x7@gmail.com>
* bump pom version for release

* revert pom version

* fix: docker release workflow to run on tag push

* fix: update tag ref in docker release workflow

* fix: update temurine version in DockerFile to match pom version 25

* fix: configure maven-jar-plugin to find the main class for manifest
* Add test for valid GET request and fix key case-sensitivity in header parsing

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Verify empty request throws exception

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Verify malformed request throws exception

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Verify valid query string

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Verify null and empty requests throws exception

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Verify malformed header line in get request throws exception

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Verify valid post request

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Verify invalid or negative content length throws exception

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Add test documentation and improve test readability for HttpParser

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Update tests and ensure UTF-8 handling; use case-insensitive headers in HttpParser

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Refactor: Introduce constant for "Content-Length" header in HttpParser

---------

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
…ons (#52)

* add application properties and a ConfigLoader to load set configurations

* integrate ConfigLoader into ServerLogging to read logging.level

* inject Server port from ConfigLoader into constructor
* test: add global filter execution test

* feat: implement global filter support with ordering

* test: add route specific filter test

* feat: add route specific filter matching logic

* update main comments with upcoming filter impl

* chore: run spotless:apply to fix unused imports and formatting

* bunny-fix

* refactor: optimize route filter lookup with Map structure

* small fix

* fix: make global filter cache thread-safe and immutable

* implement the correct plugins so server works as intended and move comments to a doc

---------

Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
* Add IP filter skeleton with placeholder IP

* Add IP filter and support client IP in HttpRequest

* Add remote IP to HttpRequest in ConnectionHandler

* Configured IP filter whitelist for localhost

* Enable IP filter with open access for development

* Update tests for HttpRequest remote IP failed

* Document IP filter configuration in App

* Resolved merge conflict in App pipeline configuration
…eadme.md (#63)

* Update index.html to include a nav bar at the top, styles.css for adjustments to frontend aspects, add readme.html to host readme.md, basic javascript for loading markdown language content.

* Update frontend logic to avoid errors loading data from readme.md. Smoothed out transitions between sites from navigation.

* Update minor bugs and duplicate css code lines.

* Update minor bugs and duplicate css code lines.

* Update for sanitization of readme.md through DOMPurify. Added dependency min.js and its callback in index.html and readme.html. Removed superfluous markdown.js.
* Add Maven Shade Plugin for building a fat JAR; update README with detailed project overview and usage instructions

* Update project version to 1.0.2-beta in pom.xml

* Update README to reflect fat JAR packaging using Maven Shade Plugin

* fix: specify final jar name so the DockerBuild picks the correct one, rollback pom version

* rollback POM version to original

* update README with dynamic tag and correct port number

* update README with the dynamic specified jar file generated from maven-shaded

---------

Co-authored-by: WHITEROSE <firasmoussa60@gmail.com>
* Refactor plugin handling by introducing `Router` abstraction; added `SimpleRouter` implementation. Updated pipeline and tests to support new routing system.

* Refactor plugin handling by introducing `Router` abstraction; added `SimpleRouter` implementation. Updated pipeline and tests to support new routing system.

* Improve wildcard routing in `SimpleRouter` with longest-prefix match logic; add test coverage for specific and wildcard match scenarios.
* feat: add filter scope annotations (@global, @route)

* Rename paths() to value() in @route for cleaner usage
* add testcontainers dependency to pom.xml

* adds 2 simple integration tests usiing Testcontainers to AppIT.java

* changes from *.jar to app.jar in JAR path in Dockerfile to avoid copy conflict

* changes AppIT.java according to code rabbit suggestion
* Add Bucket4j dependency to pom.xml for rate-limiting support

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Introduce RateLimitingFilter with Bucket4j for per-IP request throttling

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Work In Progress: Implement per-IP rate-limiting logic in RateLimitingFilter using Bucket4j and add response handling for rate limit exceeded

* Finalize and integrate RateLimitingFilter with improved logging, validation, and server cleanup. Add to App pipeline and configure properties.

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Add unit tests for RateLimitingFilter to verify per-IP request handling, rate limit enforcement, and cleanup behavior

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Refactor RateLimitingFilter and tests: simplify comments in filter, improve test method naming, and add validation test

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Add Javadoc to RateLimitingFilter and its tests for improved clarity and documentation

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Remove StaticFilesPlugin from the App pipeline configuration

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Make RateLimitingFilter configuration dynamic and improve response handling in tests

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Add rate-limiting configuration to ConfigLoader and update App pipeline to use dynamic values

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Increase rate-limiting burst capacity to 100 in configuration properties

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

* Add configurable flag to enable/disable rate-limiting in App pipeline and ConfigLoader

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

---------

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
annikaholmqvist94 and others added 9 commits February 18, 2026 11:01
* Add `RedirectRule` class to handle URL redirection logic with support for exact and wildcard path matching

* Add `RedirectFilter` to handle HTTP URL redirection logic using configurable rules

* WIP: Save current work

* Fix HttpResponse empty constructor to initialize fields

Initialize headers map and body array in empty constructor to prevent
NullPointerException when setHeader() or setBody() are called.

Before:
  HttpResponse() {}  // headers = null, body = null

After:
  HttpResponse() {
      this.headers = new LinkedHashMap<>();
      this.body = new byte[0];
  }

This fix is required for RedirectFilter and any other code that creates
empty HttpResponse objects and modifies them using setters.

Fixes crashes in RedirectFilterTest.

* Improve wildcard matching in `RedirectRule` by using `Pattern.quote` for safer and more precise regex generation.

* Update `RedirectFilterTest` to include client IP address in test request creation

---------

Co-authored-by: Kristina M <kristina0x7@gmail.com>
* remove sourcemapping url comment from the JS file

* remove stack trace from the 404 log message to console
* Add SecurityHeadersFilter for hardened HTTP responses

* Add SecurityHeadersFilter for hardened HTTP responses

* Changed X-XSS-Protection value to recommended 0,

* address code review feedback from CodeRabbit

* Add @global annotation to SecurityHeadersFilter for automatic registration

* Removed line of code in App.java
…and LoggingFilter to use the logger, ensuring connection ID is included in the logs
…ad of System.out, reflecting recent changes to LoggingFilter
…ad of System.out, reflecting recent changes to LoggingFilter (#95)
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a complete, production-ready Java HTTP server implementation with multi-stage Docker builds, GitHub Actions CI/CD pipelines, comprehensive test coverage, and a web-based UI. The project includes request/response handling, filter chains, routing, static file serving, rate limiting, security headers, logging, and configuration management via YAML.

Changes

Cohort / File(s) Summary
Build & Docker Setup
pom.xml, Dockerfile, .dockerignore, mvnw, mvnw.cmd, .mvn/wrapper/maven-wrapper.properties
Maven coordinates updated to org.juv25d/JavaHttpServer with Java 25 support. Maven wrapper scripts added for cross-platform builds. Multi-stage Dockerfile builds optimized JAR and packages into lightweight Alpine runtime.
CI/CD Workflows
.github/workflows/ci.yml, .github/workflows/docker-release.yml
CI workflow runs tests and Spotless checks on push/PR. Docker release workflow builds and pushes multi-architecture images (linux/amd64, linux/arm64) to GHCR on version tags.
Development Configuration
.editorconfig, .gitignore
Editor configuration standardizes formatting (UTF-8, LF, 4-space indent, 2-space for YAML). .gitignore expanded to include META-INF.
Core HTTP Server
src/main/java/org/juv25d/App.java, Server.java, ConnectionHandler.java, ConnectionHandlerFactory.java, DefaultConnectionHandlerFactory.java
App bootstraps server with configurable port, pipeline with global/route filters, and router. Server accepts connections on virtual threads. ConnectionHandler processes per-connection request/response lifecycle.
HTTP Protocol Layer
src/main/java/org/juv25d/http/Http*.java
HttpParser parses raw HTTP requests with validation. HttpRequest/HttpResponse are DTO records/classes. HttpResponseWriter serializes responses with proper headers and Content-Length computation.
Filter Framework
src/main/java/org/juv25d/filter/Filter.java, FilterChain.java, FilterChainImpl.java, FilterRegistration.java
Composable filter pipeline with global and route-scoped registration. Filters execute in priority order before request reaches router. Annotations (@Global, @Route) provide metadata.
Security & Standard Filters
src/main/java/org/juv25d/filter/SecurityHeadersFilter.java, IpFilter.java, LoggingFilter.java, RateLimitingFilter.java, RedirectFilter.java, RedirectRule.java
SecurityHeadersFilter injects OWASP-recommended headers. IpFilter enforces whitelist/blacklist. LoggingFilter logs requests. RateLimitingFilter uses Bucket4J per-IP rate limiting. RedirectFilter handles URL redirects with wildcard support.
Routing & Plugins
src/main/java/org/juv25d/router/Router.java, SimpleRouter.java, src/main/java/org/juv25d/plugin/Plugin.java, StaticFilesPlugin.java, NotFoundPlugin.java, HelloPlugin.java
SimpleRouter resolves paths to plugins via exact/wildcard matching. StaticFilesPlugin delegates to StaticFileHandler. NotFoundPlugin provides 404 response.
Static File Serving
src/main/java/org/juv25d/handler/StaticFileHandler.java, MimeTypeResolver.java
StaticFileHandler serves classpath resources with path traversal protection and proper MIME types. MimeTypeResolver maps extensions to MIME types with fallback.
Configuration & Logging
src/main/java/org/juv25d/util/ConfigLoader.java, src/main/java/org/juv25d/logging/*.java, src/main/resources/application-properties.yml
ConfigLoader loads YAML configuration (port, log level, rate limiting). ServerLogging configures Java logging with custom formatter. LogContext provides thread-local connection ID for request tracing.
Static Web Assets
src/main/resources/static/index.html, readme.html, css/styles.css, js/app.js
Single-page application with client-side routing, Markdown readme rendering via marked and DOMPurify, responsive design with CSS variables and animations.
Documentation
README.md, docs/adr/README.md, docs/adr/ADR-001-static-file-serving-architecture.md, docs/adr/TEMPLATE.md, docs/notes/pipeline-usage.md, src/main/resources/static/README.md
Comprehensive ADR process documentation, static file serving architecture decision record, pipeline usage examples, and project overview with deployment/development instructions.
Test Suite
src/test/java/org/juv25d/ (comprehensive coverage)
2000+ lines of JUnit 5 tests covering HTTP parsing, filters (logging, rate limiting, redirects, global/route execution), routing, static file handling, logging context, plugins, and integration tests via Testcontainers.
Removed Code
src/main/java/org/example/App.java, src/test/java/org/example/AppIT.java
Old template code removed.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Server
    participant ConnectionHandler
    participant Pipeline
    participant FilterChain as FilterChain<br/>(Global Filters)
    participant RouterPlugins as Router & Plugins
    
    Client->>Server: TCP connection on port 8080
    Server->>ConnectionHandler: create Runnable via factory
    Server->>ConnectionHandler: start on virtual thread
    ConnectionHandler->>ConnectionHandler: parse InputStream to HttpRequest
    ConnectionHandler->>Pipeline: createChain(HttpRequest)
    Pipeline->>FilterChain: build with sorted global filters<br/>+ route-specific filters
    ConnectionHandler->>FilterChain: doFilter(request, response)
    loop Global Filters (SecurityHeaders, Logging, RateLimit, etc.)
        FilterChain->>FilterChain: execute next filter in sequence
        Note over FilterChain: Each filter may<br/>modify request/response
    end
    FilterChain->>RouterPlugins: chain exhausted, resolve(request)
    RouterPlugins->>RouterPlugins: match exact path or wildcard
    RouterPlugins->>RouterPlugins: invoke plugin.handle(request, response)
    Note over RouterPlugins: e.g., StaticFilesPlugin,<br/>NotFoundPlugin
    RouterPlugins-->>FilterChain: plugin populates response
    FilterChain-->>ConnectionHandler: response ready
    ConnectionHandler->>ConnectionHandler: HttpResponseWriter.write(out, response)
    ConnectionHandler-->>Client: HTTP response with headers & body
    ConnectionHandler->>ConnectionHandler: cleanup LogContext
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Issue #89: Main changes include SecurityHeadersFilter implementation that registers as a global filter with order 0 in the App class, directly addressing the security headers feature request.

Poem

🐰 A server springs forth from the warren deep,
With filters so fine and routers to keep,
Through Docker it travels, secure and fast,
From request to response, from first until last! 🚀
Configuration and logging dance all around,
A Java HTTP jewel, production-bound!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Closes#95' is vague and does not describe the actual changes made in the changeset, which is a comprehensive Java HTTP server implementation with extensive infrastructure, filters, routing, and static file serving. Consider providing a more descriptive title that summarizes the main changes, such as 'Implement Java HTTP server with filters, routing, and static file serving' or 'Add complete HTTP server implementation with CI/CD and Docker support'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Tyreviel Tyreviel closed this Feb 19, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (33)
docs/notes/pipeline-usage.md-32-35 (1)

32-35: ⚠️ Potential issue | 🟡 Minor

Priority collision: RateLimitFilter and JwtAuthFilter share priority 100 on /api/*

Both filters are documented with the same priority (100) for the /api/* path pattern:

pipeline.addRouteFilter(new RateLimitFilter(1000, 60000), 100, "/api/*");   // priority 100
pipeline.addRouteFilter(new JwtAuthFilter(),              100, "/api/*");   // priority 100

Same-priority filters on the same route create undefined execution order. The typical intent is rate-limiting before auth (so unauthenticated requests still consume their rate-limit budget and expensive auth logic isn't invoked for blocked clients). If your Pipeline uses stable insertion-order tie-breaking this may work today, but it's a fragile assumption. Consider separating the priorities explicitly, for example:

-        // pipeline.addRouteFilter(new RateLimitFilter(1000, 60000), 100, "/api/*");
+        // pipeline.addRouteFilter(new RateLimitFilter(1000, 60000),  90, "/api/*");  // runs before auth
         // pipeline.addRouteFilter(new JwtAuthFilter(), 100, "/api/*");

A brief comment explaining the priority ordering convention (lower = earlier vs. higher = earlier) would also prevent future misconfigurations.

Also applies to: 48-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/notes/pipeline-usage.md` around lines 32 - 35, Two filters
(RateLimitFilter and JwtAuthFilter) are shown with the same priority (100) for
the same route pattern ("/api/*"), causing undefined ordering; change the
pipeline.addRouteFilter calls so RateLimitFilter has a higher precedence than
JwtAuthFilter (e.g., set RateLimitFilter priority to run before JwtAuthFilter)
and add a short comment near the pipeline registration documenting the priority
convention used (explicitly state whether lower numbers run earlier or later) to
avoid future collisions; update both occurrences referenced (the /api/*
registrations around the earlier and later blocks) to use distinct priorities
and the new comment.
docs/notes/pipeline-usage.md-25-25 (1)

25-25: ⚠️ Potential issue | 🟡 Minor

Clarify or remove the "/admin/**" recursive wildcard caveat

The (if implemented) qualifier signals that this pattern may silently not work in the current router. A developer who uncomments a filter using "/admin/**" expecting recursive matching could end up with either no matches or subtly wrong behavior.

Either confirm implementation and remove the qualifier, or replace the example with a supported pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/notes/pipeline-usage.md` at line 25, The docs note uses the ambiguous
pattern "/admin/**" with an "(if implemented)" caveat which risks misleading
callers; either confirm and remove the qualifier by updating the docs to assert
that "/admin/**" is supported, or replace the example with a supported pattern
(e.g., "/admin/*" or whatever the router actually matches) and remove the
parenthetical; locate the string "/admin/**" in pipeline-usage.md and update the
example and surrounding sentence so it accurately reflects the router's
implemented matching behavior.
Dockerfile-15-17 (1)

15-17: ⚠️ Potential issue | 🟡 Minor

Add an EXPOSE directive and resolve the hardcoded JAR name

Two minor gaps in the runtime stage:

  1. No EXPOSE instruction — the listening port isn't self-documented in the image. Add EXPOSE <port> to signal the expected port to tooling and operators.
  2. The hardcoded app.jar path (acknowledged in the TODO comment) must match the finalName in pom.xml (e.g., from the Maven Shade plugin). If they diverge the COPY will silently succeed but the entrypoint will fail at runtime with Unable to access jarfile app.jar.

Would you like me to verify the finalName configured in your pom.xml and generate the corrected COPY path?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 15 - 17, Add an EXPOSE line to document the service
port (e.g., EXPOSE 8080 or the actual port your app listens on) and make the
runtime-stage JAR path consistent with your Maven build: check the Maven pom.xml
(finalName or the Shade plugin’s output name) and update the COPY --from=build
source to that exact artifact (or make ENTRYPOINT reference the artifact name
you copy) so the jar path used by COPY and ENTRYPOINT match and avoid "Unable to
access jarfile app.jar" at runtime.
.github/workflows/ci.yml-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor

actions/checkout@v5 is outdated and inconsistent with docker-release.yml

The current latest version of actions/checkout is v6, while this workflow pins v5. The Docker release workflow in this same PR uses actions/checkout@v6.0.2, creating an inconsistency between the two CI workflows.

🔧 Proposed fix
-        uses: actions/checkout@v5
+        uses: actions/checkout@v6
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 14, The workflow pins actions/checkout@v5
which is outdated and inconsistent with the other CI workflow; update the
checkout action in the CI workflow by replacing actions/checkout@v5 with the
current version used elsewhere (e.g., actions/checkout@v6.0.2) so both CI and
docker-release workflows use the same actions/checkout version; ensure any
required inputs remain compatible with the newer action.
src/main/resources/static/README.md-134-145 (1)

134-145: ⚠️ Potential issue | 🟡 Minor

Port mismatch between README and configuration.

The README documents port 3000 (lines 134, 143, 145, 163), but src/main/resources/application-properties.yml configures port: 8080. One of these needs to be updated to match the other to avoid confusing users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/README.md` around lines 134 - 145, The README
documents the app running on port 3000 while the application config sets port:
8080, causing a mismatch; pick the intended runtime port and make them
consistent by either updating the README to show the configured port (change
documented URLs and docker run -p mapping from 3000 to 8080) or updating the
application configuration (change port: 8080 to port: 3000) so the documented
URL and docker run port mapping match the configured server port; ensure
references to the port value (e.g., the docker run -p mapping and the README
URL) and the application property "port: 8080" are updated together.
src/main/java/org/juv25d/http/HttpParser.java-61-61 (1)

61-61: ⚠️ Potential issue | 🟡 Minor

readNBytes may return fewer bytes than requested without error.

If the connection is closed prematurely, readNBytes(length) returns a shorter array silently. The parsed HttpRequest would then contain a truncated body with no indication of the problem. Consider validating body.length == length.

Proposed fix
         body = in.readNBytes(length);
+        if (body.length != length) {
+            throw new IOException("Incomplete body: expected " + length + " bytes, got " + body.length);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/http/HttpParser.java` at line 61, The call to
in.readNBytes(length) in HttpParser can return fewer bytes than requested if the
connection closes; update the parsing code that assigns body to validate that
body.length == length after the read, and treat a short read as an error: throw
or return a failed parse (e.g., IllegalStateException or a ParseException)
and/or log and close the connection. Locate the assignment to body (body =
in.readNBytes(length)) in class HttpParser and add a check for the expected
length, handling the mismatch by failing the request parse rather than accepting
a truncated body.
src/main/resources/static/readme.html-18-18 (1)

18-18: ⚠️ Potential issue | 🟡 Minor

HTML entities missing terminating semicolons.

&#128269 and &#128270 should be &#128269; and &#128270;. Without the semicolons, these are technically malformed character references per the HTML5 spec and may not render in strict parsers.

🔧 Proposed fix
-        <h1>&#128269 ReadMe &#128270</h1>
+        <h1>&#128269; ReadMe &#128270;</h1>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/readme.html` at line 18, The HTML character
references in the H1 element are missing terminating semicolons; update the H1
text node that currently contains "&#128269" and "&#128270" to use "&#128269;"
and "&#128270;" respectively so the character references are well-formed (locate
the <h1> element containing these entities and add the semicolons).
src/main/resources/static/readme.html-37-37 (1)

37-37: ⚠️ Potential issue | 🟡 Minor

Inconsistent script path — missing leading / on purify.min.js.

All other static assets on this page use root-relative paths (/js/..., /css/...), but purify.min.js uses a bare-relative path. While functionally identical when served from /, it will silently fail if the file is ever moved or served from a subdirectory.

🔧 Proposed fix
-<script src="js/purify.min.js"></script>
+<script src="/js/purify.min.js"></script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/readme.html` at line 37, Update the script src for
purify.min.js to use a root-relative path so it matches the other static asset
URLs; locate the <script> tag that references "purify.min.js" and change its src
from "js/purify.min.js" to "/js/purify.min.js" to avoid breakage when the site
is served from a subdirectory.
src/test/java/org/juv25d/plugin/NotFoundPluginTest.java-24-24 (1)

24-24: ⚠️ Potential issue | 🟡 Minor

Specify charset explicitly in getBytes().

"404 - Resource Not Found".getBytes() uses Charset.defaultCharset(). If the JVM default is not UTF-8, this assertion can produce a false positive or false negative. Pair it with the same fix in NotFoundPlugin.java.

🔧 Proposed fix
-        assertArrayEquals("404 - Resource Not Found".getBytes(), res.body());
+        assertArrayEquals("404 - Resource Not Found".getBytes(StandardCharsets.UTF_8), res.body());

Also add the import at the top of the file:

+import java.nio.charset.StandardCharsets;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/plugin/NotFoundPluginTest.java` at line 24, Update
the assertion in NotFoundPluginTest to use an explicit charset rather than the
platform default: change the hardcoded byte comparison to use "404 - Resource
Not Found".getBytes(StandardCharsets.UTF_8) and add the corresponding import for
java.nio.charset.StandardCharsets; also make the analogous change in
NotFoundPlugin (where the response body is produced) to use
StandardCharsets.UTF_8 so both sides use the same encoding for res.body()
comparisons.
src/main/java/org/juv25d/plugin/NotFoundPlugin.java-13-13 (1)

13-13: ⚠️ Potential issue | 🟡 Minor

Specify charset in getBytes().

"404 - Resource Not Found".getBytes() uses Charset.defaultCharset(), which is platform-dependent. This can produce unexpected bytes on non-UTF-8 JVMs, and creates a fragile pairing with the test assertion in NotFoundPluginTest.

🔧 Proposed fix
+import java.nio.charset.StandardCharsets;
 ...
-        res.setBody("404 - Resource Not Found".getBytes());
+        res.setBody("404 - Resource Not Found".getBytes(StandardCharsets.UTF_8));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/plugin/NotFoundPlugin.java` at line 13, The response
body in NotFoundPlugin uses platform-default bytes via "404 - Resource Not
Found".getBytes(); change this to use an explicit charset (e.g.,
StandardCharsets.UTF_8) so the bytes are deterministic across platforms and
match tests: update the call in NotFoundPlugin (where res.setBody(...) is
invoked) to encode the string with StandardCharsets.UTF_8.
src/test/java/org/juv25d/logging/ConnectionIdLoggingTest.java-33-47 (1)

33-47: ⚠️ Potential issue | 🟡 Minor

Add logger.removeHandler(handler) to the finally block to prevent handler accumulation.

Logger.getLogger("test.connectionid") returns a shared, JVM-scoped instance. Without removing the handler, each test execution appends a new handler to the same logger. In parallel or repeated test-suite runs this causes handler leakage — prior run's handlers still capture records — making the test flaky.

🛡️ Proposed fix
         try {
             String testId = "test-123";
             LogContext.setConnectionId(testId);

             // Act
             logger.info("This is a test message");

             // Assert
             assertTrue(formattedMessages.get(0).contains("[" + testId + "]"), 
                 "Log message should contain the connection ID. Found: " + formattedMessages.get(0));
         } finally {
+            logger.removeHandler(handler);
             LogContext.clear();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/logging/ConnectionIdLoggingTest.java` around lines
33 - 47, The test is leaking handlers on the shared Logger instance; modify the
finally block in ConnectionIdLoggingTest so it not only calls LogContext.clear()
but also removes the test handler from the logger by calling
logger.removeHandler(handler) to prevent handler accumulation across test runs;
locate the try/finally around LogContext.setConnectionId/test logging and ensure
you remove the same `handler` that was added to `logger` before clearing the
context so subsequent tests don't receive stale records from
`formattedMessages`.
src/main/java/org/juv25d/plugin/HelloPlugin.java-11-13 (1)

11-13: ⚠️ Potential issue | 🟡 Minor

Placeholder handle() produces an uninitialized HttpResponse (statusCode 0, statusText null).

Until the body is implemented, any request routed to HelloPlugin will emit a malformed HTTP response. Even for scaffolding, a minimal fallback (e.g., 200 OK with a placeholder body) prevents breaking the server during integration testing.

💡 Minimal placeholder implementation
     `@Override`
     public void handle(HttpRequest req, HttpResponse res) throws IOException {
-        // placeholder response logic
+        res.setStatusCode(200);
+        res.setStatusText("OK");
+        res.setBody("Hello, World!".getBytes(java.nio.charset.StandardCharsets.UTF_8));
+        res.setHeader("Content-Type", "text/plain; charset=UTF-8");
     }

Would you like me to generate a more complete implementation for this plugin?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/plugin/HelloPlugin.java` around lines 11 - 13, The
handle(HttpRequest req, HttpResponse res) method in HelloPlugin currently leaves
HttpResponse uninitialized, producing statusCode 0 and statusText null; update
HelloPlugin.handle to set a minimal fallback response (e.g., set status code to
200, status text to "OK", appropriate Content-Type, and a small placeholder body
like "Hello from HelloPlugin") and ensure headers and body length are correctly
set on res so any routed request returns a well-formed HTTP response during
integration tests.
src/main/resources/static/css/styles.css-63-132 (2)

63-132: ⚠️ Potential issue | 🟡 Minor

declaration-empty-line-before Stylelint errors throughout .nav-menu block — multiple [error] violations.

Stylelint 17.3.0 flags empty lines immediately before declarations on lines 65, 72, 88, 90–105. These should be resolved by removing the blank lines or adjusting the block-level comment placement to separate concerns without triggering the rule.

The most impactful instances (lines 72, 88, 90–105) can be addressed by moving anchor-name and position-anchor to immediately follow the previous declaration:

📐 Representative fix for lines 63–75
 .nav-menu {
-
     width: fit-content;
     margin: 0.4rem auto 1rem auto;
     display: flex;
     align-content: center;
     justify-content: center;
     isolation: isolate;
-
     anchor-name: --hovered-link;

Similar blank-line removal is needed at lines 88, 183, 212, 226, 228, 230–231, and 246 per the full Stylelint report.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/styles.css` around lines 63 - 132, Stylelint is
flagging empty lines before declarations inside the .nav-menu block; remove the
blank lines that precede declarations (or reposition block-level comments) so
declarations are contiguous—specifically move anchor-name: --hovered-link; and
position-anchor: --hovered-link; to immediately follow the preceding declaration
in the .nav-menu rule and in the &::before / &::after rules, and eliminate the
stray blank lines before other properties in ::before, ::after and
&:has(a:hover)::before / ::after so no declaration is separated by an empty
line.

63-132: ⚠️ Potential issue | 🟡 Minor

Nav hover effect uses CSS Anchor Positioning without an @supports guard.

The .nav-menu and .features blocks use anchor-name, position-anchor, and anchor() (lines 72, 75, 88, 122–125, 213, 215, 228) without @supports guards. As of February 2026, anchor-name and the anchor() function are Baseline Newly available, but position-anchor remains in limited availability. In Firefox, the feature is enabled by default only in version 147+; Firefox 145–146 requires a flag. Wrapping these declarations in @supports (position-anchor: top) or similar (as Chrome's documentation recommends) makes the intent explicit and future-proof, ensuring the decorative hover effect gracefully degrades in unsupported browsers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/styles.css` around lines 63 - 132, Wrap all
anchor positioning properties and anchor() usage inside a feature-detection
block so they gracefully degrade: add an `@supports` (position-anchor: top) guard
around the rules in .nav-menu that use anchor-name, position-anchor, and the
anchor() function (including the li:hover anchor-name, the &::before/&::after
top/left/right/bottom values that call anchor(), and the &:has(a:hover) block)
so those declarations only apply when position-anchor is supported and browsers
without it ignore them.
src/main/resources/static/css/styles.css-108-115 (1)

108-115: ⚠️ Potential issue | 🟡 Minor

Stylelint comment-whitespace-inside errors on lines 111 and 156 — add spaces inside comment delimiters.

Stylelint reports these as [error]-level violations.

🛠️ Proposed fix
-        /*backdrop-filter: blur(2px);*/
+        /* backdrop-filter: blur(2px); */
-    }*/
+    } */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/styles.css` around lines 108 - 115, Stylelint
flags comment-whitespace-inside for comments inside the &::before and &::after
blocks; update inline CSS comments to include a single space after /* and before
*/ (e.g., change /*backdrop-filter: blur(2px);*/ to /* backdrop-filter:
blur(2px); */) and apply the same spacing convention to the other commented line
referenced in the &::after block so both comments conform to the
comment-whitespace-inside rule.
src/main/resources/static/index.html-47-47 (1)

47-47: ⚠️ Potential issue | 🟡 Minor

Missing leading / in purify.min.js script path.

All other static asset references (/css/styles.css, /js/marked.min.js, /js/app.js) use absolute paths. This one uses a relative path, which works when served from / but silently breaks under any sub-path routing or redirect.

🐛 Proposed fix
-<script src="js/purify.min.js"></script>
+<script src="/js/purify.min.js"></script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/index.html` at line 47, The script tag referencing
purify.min.js in index.html uses a relative path "js/purify.min.js" which breaks
under sub-path routing; update the script's src attribute so it uses an absolute
path starting with a leading slash (consistent with the other assets like
/css/styles.css and /js/app.js) to point to /js/purify.min.js.
src/main/java/org/juv25d/handler/StaticFileHandler.java-16-22 (1)

16-22: ⚠️ Potential issue | 🟡 Minor

Javadoc describes the resource root as /resources/static/ but the code maps to /static/.

STATIC_DIR = "/static/" is the correct classpath path (Maven strips the src/main/resources prefix). The Javadoc examples (GET / → /resources/static/index.html) are misleading.

📝 Suggested correction
- * Handles serving static files from the /resources/static/ directory.
+ * Handles serving static files from the classpath path /static/
+ * (corresponding to src/main/resources/static/ in the Maven source tree).
  * <p>
  * URL mapping:
- * - GET / → /resources/static/index.html
- * - GET /about.html → /resources/static/about.html
- * - GET /css/styles.css → /resources/static/css/styles.css
+ * - GET / → /static/index.html
+ * - GET /about.html → /static/about.html
+ * - GET /css/styles.css → /static/css/styles.css
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/handler/StaticFileHandler.java` around lines 16 -
22, The Javadoc incorrectly refers to "/resources/static/" while the code uses
STATIC_DIR = "/static/"; update the Javadoc in StaticFileHandler to reference
the actual classpath root "/static/" and adjust the URL mapping examples (e.g.,
"GET / → /static/index.html", "GET /about.html → /static/about.html", "GET
/css/styles.css → /static/css/styles.css") so they match the STATIC_DIR constant
and real runtime behavior.
src/test/java/org/juv25d/handler/StaticFileHandlerTest.java-13-20 (1)

13-20: ⚠️ Potential issue | 🟡 Minor

Tests asserting 200 depend on actual classpath resources—brittle and prone to silent failures.

shouldReturn200ForExistingFile, shouldReturnCorrectContentTypeForHtml/Css/Js, shouldServeIndexHtmlForRootPath, and shouldReturnNonEmptyBodyForSuccessfulRequest all invoke StaticFileHandler.handle and assert 200 responses. They depend on real files at src/main/resources/static/index.html, css/styles.css, and js/app.js. If these files are deleted or renamed, tests will fail silently with 404 assertions rather than signaling test framework errors.

Use either (a) test-only resource fixtures under src/test/resources/static/, or (b) mock the resource loading path to decouple tests from filesystem state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/handler/StaticFileHandlerTest.java` around lines 13
- 20, The tests (shouldReturn200ForExistingFile,
shouldReturnCorrectContentTypeForHtml/Css/Js, shouldServeIndexHtmlForRootPath,
shouldReturnNonEmptyBodyForSuccessfulRequest) are brittle because they rely on
real classpath files; update the tests to use test fixtures or mocking: either
add the required files (index.html, css/styles.css, js/app.js) under
src/test/resources/static/ so StaticFileHandler.handle(request) finds them
during tests, or refactor the handler to allow injecting a
ResourceLoader/ClasspathResolver and in tests mock that dependency to return
controlled InputStreams for those paths; make the change against the test
methods and StaticFileHandler so assertions asserting 200 no longer depend on
production resources.
src/test/java/org/juv25d/handler/StaticFileHandlerTest.java-60-62 (1)

60-62: ⚠️ Potential issue | 🟡 Minor

new String(response.body()) without specifying charset.

The response body is UTF-8 encoded (set explicitly in StaticFileHandler). Relying on the platform default charset makes these assertions non-deterministic on systems where the default is not UTF-8.

♻️ Proposed fix (applies to lines 61, 62, and 72)
-        assertThat(new String(response.body())).contains("404");
-        assertThat(new String(response.body())).contains("Not Found");
+        assertThat(new String(response.body(), StandardCharsets.UTF_8)).contains("404");
+        assertThat(new String(response.body(), StandardCharsets.UTF_8)).contains("Not Found");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/handler/StaticFileHandlerTest.java` around lines 60
- 62, In StaticFileHandlerTest update the assertions that build Strings from
response.body() to specify UTF-8 explicitly (e.g., replace occurrences of new
String(response.body()) with new String(response.body(),
StandardCharsets.UTF_8)) so the tests are deterministic; ensure you import
java.nio.charset.StandardCharsets and apply this change for the assertions
checking "404", "Not Found" and any other place (line with 72) that converts
response.body() to a String.
src/main/java/org/juv25d/filter/RateLimitingFilter.java-109-118 (1)

109-118: ⚠️ Potential issue | 🟡 Minor

Client IP leaked in 429 response body.

Line 110 includes the client's IP address in the HTTP response body. This is a minor information disclosure — the client (or any intermediary) learns how the server perceives their IP. Remove the IP from the user-visible response and keep it in the server-side log only (which you already do in logRateLimitExceeded).

Proposed fix
-        byte[] body = ("429 Too Many Requests: Rate limit exceeded for IP " + ip + "\n")
+        byte[] body = "429 Too Many Requests: Rate limit exceeded\n"
             .getBytes(StandardCharsets.UTF_8);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/RateLimitingFilter.java` around lines 109 -
118, The response body in sendTooManyRequests currently leaks the client's IP;
change the user-visible message to a generic text (e.g., "429 Too Many Requests:
Rate limit exceeded") and remove the ip variable from the body while leaving
headers/status intact; keep the existing logRateLimitExceeded call (or ensure
server-side logging still records the ip) so the IP is only present in server
logs, not in the HTTP response body.
src/main/java/org/juv25d/filter/RateLimitingFilter.java-83-91 (1)

83-91: ⚠️ Potential issue | 🟡 Minor

Use the builder API instead of the deprecated Bandwidth.classic() factory.

The project uses Bucket4j 8.16.1. While Refill.intervally() is still supported, Bandwidth.classic() is deprecated in 8.x. Replace the deprecated pattern with the builder API:

private Bucket createBucket() {
    return Bucket.builder()
        .addLimit(limit -> limit
            .capacity(capacity)
            .refillIntervally(refillTokens, refillPeriod))
        .build();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/RateLimitingFilter.java` around lines 83 -
91, Replace the deprecated Bandwidth.classic usage inside createBucket(): stop
creating a Bandwidth via Bandwidth.classic(capacity, Refill.intervally(...)) and
instead use the Bucket.builder().addLimit(...) builder API; inside addLimit use
the lambda to set capacity(capacity) and refillIntervally(refillTokens,
refillPeriod) (instead of Refill.intervally), then build the Bucket. Update
references to Bandwidth.classic and keep the existing refillTokens, refillPeriod
and capacity variables.
docs/adr/ADR-001-static-file-serving-architecture.md-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

ADR status should be updated to reflect current state.

The implementation described in this ADR already exists in the PR (StaticFileHandler, MimeTypeResolver, tests, etc.). Update the status from Proposed to Accepted (or Implemented) so the ADR accurately reflects the decision lifecycle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/adr/ADR-001-static-file-serving-architecture.md` at line 4, Update the
ADR status line in ADR-001-static-file-serving-architecture.md from "Proposed"
to "Accepted" (or "Implemented") to reflect the actual implementation; ensure
the document header or status field is changed and optionally add a brief note
that StaticFileHandler, MimeTypeResolver and accompanying tests have been
implemented so the ADR accurately matches the existing codebase.
README.md-39-39 (1)

39-39: ⚠️ Potential issue | 🟡 Minor

Multiple fenced code blocks are missing a language identifier (markdownlint MD040).

Lines 39, 141, 156, 162, 174, 269, 322, 364, 370, and 386 open triple-backtick blocks without a language hint. Use text or plaintext for diagrams/paths and bash/yaml/java for the appropriate blocks to silence these MD040 warnings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 39, Several fenced code blocks in README.md are missing
language identifiers (MD040); open triple-backtick blocks at the noted locations
should be updated to include an appropriate language tag. Edit each offending
block (the triple-backtick fences) and add a language hint such as text or
plaintext for diagrams/paths, bash for shell commands, yaml for YAML snippets,
or java for Java code so the fences (```...) become ```text, ```bash, ```yaml,
etc., to silence the markdownlint MD040 warnings.
docs/adr/ADR-001-static-file-serving-architecture.md-238-238 (1)

238-238: ⚠️ Potential issue | 🟡 Minor

Replace placeholder URL with the actual repository link.

Line 238 contains https://github.com/your-repo/issues/18. Update to the real repository URL before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/adr/ADR-001-static-file-serving-architecture.md` at line 238, Replace
the placeholder repository link "https://github.com/your-repo/issues/18" used in
the markdown link "[Issue `#18`: GET handling for static files]" with the actual
repository issue URL (e.g., "https://github.com/ORG/REPO/issues/18") so the
reference resolves correctly; update the single occurrence of that placeholder
URL in ADR-001-static-file-serving-architecture.md to the real repo path before
merging.
README.md-178-178 (1)

178-178: ⚠️ Potential issue | 🟡 Minor

Heading levels skip from h1 to h3 (markdownlint MD001).

Lines 178 (### Example Mapping) and 251 (### Supported Patterns) follow an h1 section directly — violating the one-level-at-a-time increment rule. Promote the section above to ## or demote the subsection to ## to fix both.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 178, The README uses level-3 headings "### Example
Mapping" and "### Supported Patterns" directly after an h1, causing markdownlint
MD001; update those headings to level-2 (change "### Example Mapping" and "###
Supported Patterns" to "## Example Mapping" and "## Supported Patterns") so the
document increments heading levels one at a time.
src/main/java/org/juv25d/filter/RedirectRule.java-30-35 (1)

30-35: ⚠️ Potential issue | 🟡 Minor

Missing null validation for sourcePath and targetUrl constructor parameters.

If either is null, sourcePath.contains("*") in matches() (or getTargetUrl() usage in RedirectFilter) will NPE at runtime. A simple guard in the constructor prevents this class of bugs:

🛡️ Proposed fix
 public RedirectRule(String sourcePath, String targetUrl, int statusCode) {
     validateStatusCode(statusCode);
+    Objects.requireNonNull(sourcePath, "sourcePath must not be null");
+    Objects.requireNonNull(targetUrl, "targetUrl must not be null");
     this.sourcePath = sourcePath;
     this.targetUrl = targetUrl;
     this.statusCode = statusCode;
 }

Add import java.util.Objects; to the imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/RedirectRule.java` around lines 30 - 35, The
RedirectRule constructor lacks null checks for sourcePath and targetUrl which
can cause NPEs in matches() (sourcePath.contains("*")) and when getTargetUrl()
is used; update the RedirectRule(String sourcePath, String targetUrl, int
statusCode) constructor to validate non-null inputs (e.g., using
Objects.requireNonNull or equivalent) for sourcePath and targetUrl before
assigning fields and still call validateStatusCode(statusCode); add the
necessary import (java.util.Objects) if you use Objects.requireNonNull and
ensure error messages clearly state which parameter is null.
src/main/java/org/juv25d/util/ConfigLoader.java-9-12 (1)

9-12: ⚠️ Potential issue | 🟡 Minor

Missing field defaults when YAML sections are absent.

If the server or logging section is entirely absent from application-properties.yml, the null-guard if (serverConfig != null) silently skips the block, leaving port = 0, rootDirectory = null, and logLevel = null. port = 0 causes the server to bind to a random ephemeral port; null values will cause NPEs downstream in ServerLogging and static-file serving.

Initialize the fields to sensible defaults at declaration time so they remain correct even when the YAML section is omitted:

🛡️ Proposed fix
-    private int port;
-    private String logLevel;
-    private String rootDirectory;
+    private int port = 8080;
+    private String logLevel = "INFO";
+    private String rootDirectory = "static";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/util/ConfigLoader.java` around lines 9 - 12, The
ConfigLoader class currently leaves port, logLevel, and rootDirectory
uninitialized when YAML sections are missing, causing port=0 and null NPEs;
initialize the fields at declaration in ConfigLoader (port, logLevel,
rootDirectory) to sensible defaults (e.g., port 8080, logLevel "INFO",
rootDirectory "./public" or another project default) so that when serverConfig
or logging maps are null the rest of the code (including ServerLogging and
static-file serving) has safe fallback values; update any related null-guards in
the init logic to only override these defaults when values are present in the
parsed serverConfig/logging maps.
src/main/java/org/juv25d/http/HttpResponseWriter.java-40-41 (1)

40-41: ⚠️ Potential issue | 🟡 Minor

Content-Length is always emitted, including for status codes that forbid it.

RFC 7230 §3.3.2 prohibits a server from sending Content-Length in 204 No Content and 304 Not Modified responses (and 1xx responses). The current implementation unconditionally writes Content-Length: 0 for these codes, which is a protocol violation that some strict clients or proxies may reject.

🛡️ Suggested guard
+    private static final java.util.Set<Integer> NO_BODY_STATUSES =
+        java.util.Set.of(204, 304);

     private static void writeHeaders(
         OutputStream out,
         Map<String, String> headers,
         byte[] body
     ) throws IOException {
         for (Map.Entry<String, String> header : headers.entrySet()) {
             if (!header.getKey().equalsIgnoreCase("Content-Length")) {
                 String line = header.getKey() + ": " + header.getValue() + "\r\n";
                 out.write(line.getBytes(StandardCharsets.UTF_8));
             }
         }

-        String contentLength = "Content-Length: " + body.length + "\r\n";
-        out.write(contentLength.getBytes(StandardCharsets.UTF_8));
+        // Omit Content-Length for status codes that forbid a message body
+        // (check is done on caller side by passing statusCode here, or gate in write())
         out.write("\r\n".getBytes(StandardCharsets.UTF_8));
     }

The cleanest approach is to pass response.statusCode() into writeHeaders and skip the Content-Length line when the code is 204 or 304.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/http/HttpResponseWriter.java` around lines 40 - 41,
The HttpResponseWriter currently always writes a Content-Length header using the
body byte array; change writeHeaders (or the method that writes headers in
HttpResponseWriter) to accept the response status code (or read
response.statusCode()) and skip emitting the "Content-Length" line when the
status is 1xx, 204, or 304 (i.e., do not write "Content-Length: 0" for those
codes); keep emitting Content-Length for all other statuses and use the existing
body.length logic unchanged for those cases.
docs/adr/ADR-001-static-file-serving-architecture.md-46-46 (1)

46-46: ⚠️ Potential issue | 🟡 Minor

Add language hints to fenced code blocks (markdownlint MD040).

Lines 46, 65, and 224 all open fenced code blocks without a language identifier. markdownlint-cli2 reports MD040 warnings for each. Use text or plaintext for ASCII-art diagrams to silence these.

🔧 Suggested fixes
-```
+```text
 src/main/resources/

(Same fix applies to lines 65 and 224.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/adr/ADR-001-static-file-serving-architecture.md` at line 46, Several
fenced code blocks in the ADR document open without a language identifier (the
ASCII-art directory/listing blocks); update each of those fenced blocks (the
three places that show plain text/ASCII diagrams) to use a language hint such as
```text or ```plaintext so markdownlint MD040 is satisfied—replace the opening
``` with ```text (e.g., ```text followed by src/main/resources/) for all three
occurrences.
src/test/java/org/juv25d/http/HttpParserTest.java-153-153 (1)

153-153: ⚠️ Potential issue | 🟡 Minor

Use explicit charset in body.getBytes() for platform independence.

body.getBytes() relies on the platform default charset. While "body" is ASCII-safe today, the assertion should be deterministic regardless of JVM locale.

🛡️ Proposed fix
-        assertThat(result.body()).isEqualTo(body.getBytes());
+        assertThat(result.body()).isEqualTo(body.getBytes(StandardCharsets.UTF_8));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/http/HttpParserTest.java` at line 153, The assertion
in HttpParserTest uses body.getBytes() which is platform-dependent; update the
assertion to use an explicit charset (e.g., StandardCharsets.UTF_8) so the
comparison is deterministic across JVM locales—modify the assertion that
references result.body() and body.getBytes() to call
body.getBytes(StandardCharsets.UTF_8) and add the necessary import for
StandardCharsets if missing.
src/test/java/org/juv25d/logging/ServerLoggingTest.java-84-90 (1)

84-90: ⚠️ Potential issue | 🟡 Minor

Test assumes the configured log level is INFO.

This test asserts Level.INFO on the singleton logger, but the actual level is determined by application-properties.yml (or env/system property). If the config specifies a different level, this test breaks. Consider either reading the expected level from the config or testing a freshly configured logger with a known level.

src/test/java/org/juv25d/logging/ServerLoggingTest.java-123-141 (1)

123-141: ⚠️ Potential issue | 🟡 Minor

Add null check for ConfigLoader.getLogLevel() to prevent NPE if configuration is missing.

The logLevel field in ConfigLoader is not initialized with a default value. If the logging.level key is absent from application-properties.yml, line 131 will throw NullPointerException on configLogLevel.toUpperCase(). While the current config file includes the key, the test should be defensive against configuration changes.

Proposed fix
     void logger_shouldUseLogLevelFromApplicationProperties() {
         String configLogLevel = ConfigLoader.getInstance().getLogLevel();
+        assertNotNull(configLogLevel, "Expected logging.level to be set in application-properties.yml");
 
         try {
             System.clearProperty("log.level");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/logging/ServerLoggingTest.java` around lines 123 -
141, The test should defensively handle a missing logging.level by checking
ConfigLoader.getInstance().getLogLevel() for null/blank before calling
toUpperCase(); in the test method
logger_shouldUseLogLevelFromApplicationProperties(), replace the direct use of
configLogLevel with a guarded value (e.g., if configLogLevel is null or empty
set it to "INFO") and then call Level.parse on that guarded string, leaving the
rest of the flow (Logger testLogger, ServerLogging.configure(testLogger),
assertEquals) unchanged so no NPE occurs when the config key is absent.
src/main/java/org/juv25d/Pipeline.java-43-64 (1)

43-64: ⚠️ Potential issue | 🟡 Minor

createChain() should validate that router is set before use.

While setRouter() validates non-null input, createChain() passes router directly to FilterChainImpl without checking if it has been set. This creates an API design gap: if createChain() is called before setRouter(), a NullPointerException will occur in FilterChainImpl.doFilter() (line 27) when router.resolve(req) is invoked after all filters have been processed.

Although the current application always calls setRouter() before processing requests, the defensive check improves API robustness for correct usage:

Proposed fix
     public FilterChainImpl createChain(HttpRequest request) {
+        if (router == null) {
+            throw new IllegalStateException("Router must be set before creating a filter chain");
+        }
         List<Filter> filters = new ArrayList<>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/Pipeline.java` around lines 43 - 64, createChain()
currently passes the field router into new FilterChainImpl without validating
it's been initialized; add a defensive null-check (e.g. use
Objects.requireNonNull(router, ...) or an explicit if (router == null) throw new
IllegalStateException("router not set - call setRouter() before createChain()"))
at the start of createChain() so that calling createChain() before setRouter()
fails fast and clearly (references: createChain, setRouter, router,
FilterChainImpl.doFilter).

Comment on lines +11 to +17
FROM eclipse-temurin:25-jre-alpine

WORKDIR /app

# might need to update this later when we have our explicit class names
COPY --from=build /app/target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Container runs as root — add a non-root USER directive

No USER instruction is present in the runtime stage. The application process runs as root inside the container, meaning any exploit of the server grants root-level container access. Trivy rule DS-0002 flags this explicitly.

🔒 Proposed fix — add a non-root user
 FROM eclipse-temurin:25-jre-alpine
 
 WORKDIR /app
 
+RUN addgroup -S appgroup && adduser -S appuser -G appgroup
+USER appuser
+
 # might need to update this later when we have our explicit class names
 COPY --from=build /app/target/app.jar app.jar
 ENTRYPOINT ["java", "-jar", "app.jar"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 11 - 17, The Dockerfile runtime stage currently runs
the app as root; create and switch to a non-root user before ENTRYPOINT by
adding a user (e.g., group/user creation and UID/GID), chown the application
artifacts copied by COPY --from=build (app.jar) and set USER to that non-root
user after WORKDIR and before ENTRYPOINT so the java -jar app.jar process runs
unprivileged; ensure permissions allow the user to read/execute app.jar and the
WORKDIR.

Comment on lines +49 to +60
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>2.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Testcontainers 2.0.3 junit-jupiter module version compatibility

💡 Result:

For Testcontainers Java 2.0.3, the JUnit Jupiter integration module is version-aligned with core:

  • Use org.testcontainers:testcontainers-junit-jupiter:2.0.3 together with org.testcontainers:testcontainers:2.0.3 (the 2.0.3 JUnit-Jupiter module depends on core 2.0.3). [1][2]
  • JUnit 5 itself is not brought in by that module’s POM, so you must add your own JUnit Jupiter (API/engine) dependencies (whatever JUnit 5 version your project uses). [1][3]
  • If you’re migrating from 1.x: in 2.0.x, modules were renamed to the testcontainers-* prefix (so old org.testcontainers:junit-jupiterorg.testcontainers:testcontainers-junit-jupiter). [4]

Recommended (Maven) approach: import the Testcontainers BOM at 2.0.3 and omit individual Testcontainers versions to avoid mismatches. [1]

Sources
[1] Maven Central – testcontainers-junit-jupiter:2.0.3 POM (dependency on testcontainers:2.0.3) (central.sonatype.com)
[2] Maven Central – testcontainers:2.0.3 POM (core artifact) (central.sonatype.com)
[3] Testcontainers docs – JUnit 5 (Jupiter) integration module overview (java.testcontainers.org)
[4] Testcontainers GitHub releases – 2.0.0 notes on module rename prefix testcontainers- (github.com)

Citations:


Fix Testcontainers artifact ID and version mismatch.

The junit-jupiter module uses org.testcontainers:junit-jupiter:1.21.4, which is incompatible with core testcontainers:2.0.3. Testcontainers 2.0.x renamed modules to the testcontainers-* prefix and versions are no longer aligned with 1.x. Update to org.testcontainers:testcontainers-junit-jupiter:2.0.3 to match the core artifact.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` around lines 49 - 60, The junit-jupiter dependency is using the
wrong artifactId and mismatched version; replace the dependency that currently
has <artifactId>junit-jupiter</artifactId> and <version>1.21.4</version> with
the Testcontainers 2.0.x module by changing the artifactId to
testcontainers-junit-jupiter and the version to 2.0.3 (i.e., use
org.testcontainers:testcontainers-junit-jupiter:2.0.3) so it matches the core
testcontainers artifact.

Comment on lines +59 to +61
} catch (IOException e) {
logger.log(Level.SEVERE, "Error while handling request", e);
} finally {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exceptions are swallowed without sending an HTTP error response to the client.

When httpParser.parse(in) throws (e.g., malformed request), or the pipeline throws, the catch block only logs at SEVERE and lets the try-with-resources close the socket. The client receives an abrupt connection reset instead of a proper 400 Bad Request or 500 Internal Server Error. This makes clients unable to surface or retry intelligently, and breaks browser behavior on malformed requests.

A minimal fix differentiates parse errors (→ 400) from pipeline/write errors (→ 500):

🐛 Proposed fix
+import java.util.Map;
+import java.util.logging.Level;

     `@Override`
     public void run() {
         String connectionId = java.util.UUID.randomUUID().toString().substring(0, 8);
         org.juv25d.logging.LogContext.setConnectionId(connectionId);
         try (socket) {
             var in = socket.getInputStream();
             var out = socket.getOutputStream();

+            HttpRequest parsed;
+            try {
+                parsed = httpParser.parse(in);
+            } catch (IOException e) {
+                logger.log(Level.WARNING, "Malformed request from client", e);
+                HttpResponseWriter.write(out, new HttpResponse(400, "Bad Request", Map.of(), new byte[0]));
+                return;
+            }
-            HttpRequest parsed = httpParser.parse(in);
             String remoteIp = socket.getInetAddress().getHostAddress();
             // ... rest unchanged
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/ConnectionHandler.java` around lines 59 - 61, The
current catch in ConnectionHandler swallows IOExceptions and closes the socket,
causing clients to get connection resets; update the exception handling around
httpParser.parse(in) and the pipeline/write sequence to send proper HTTP
responses: detect parse errors thrown by httpParser.parse(in) (e.g., a
HttpParseException or similar) and write a "400 Bad Request" response to the
socket output stream, while other IO/pipeline exceptions should result in a "500
Internal Server Error" response; ensure you write the response to the
OutputStream (and flush) inside its own try/catch so errors there are logged via
logger.log(Level.SEVERE, ...) but still let the try-with-resources close the
socket afterwards. Include references to httpParser.parse(in), the
pipeline/write path, and logger in your changes.

Comment on lines +8 to +22
public class DefaultConnectionHandlerFactory implements ConnectionHandlerFactory{
private final HttpParser httpParser;
private final Logger logger;
private final Pipeline pipeline;

public DefaultConnectionHandlerFactory(HttpParser httpParser, Logger logger, Pipeline pipeline) {
this.httpParser = httpParser;
this.logger = logger;
this.pipeline = pipeline;
}

@Override
public Runnable create(Socket socket, Pipeline pipeline) {
return new ConnectionHandler(socket, httpParser, logger, pipeline);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the ConnectionHandlerFactory interface
echo "=== ConnectionHandlerFactory Interface ==="
fd -t f '\.java$' | xargs rg -l "interface ConnectionHandlerFactory" 
rg -A 10 "interface ConnectionHandlerFactory" --type=java

echo -e "\n=== Usages of handlerFactory.create() ==="
rg -n 'handlerFactory\.create\(' --type=java -B 2 -A 2

echo -e "\n=== Usages of \.create\(socket' ==="
rg -n '\.create\(socket' --type=java -B 2 -A 2

echo -e "\n=== How DefaultConnectionHandlerFactory is instantiated ==="
rg -n "new DefaultConnectionHandlerFactory" --type=java -B 2 -A 2

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 1817


Remove unused pipeline field—the create() method parameter shadows it.

The constructor stores this.pipeline, but create(Socket socket, Pipeline pipeline) receives its own pipeline parameter that shadows the field. The field is never read and should be removed along with its constructor parameter.

Refactoring
 public class DefaultConnectionHandlerFactory implements ConnectionHandlerFactory{
     private final HttpParser httpParser;
     private final Logger logger;
-    private final Pipeline pipeline;
 
-    public DefaultConnectionHandlerFactory(HttpParser httpParser, Logger logger, Pipeline pipeline) {
+    public DefaultConnectionHandlerFactory(HttpParser httpParser, Logger logger) {
         this.httpParser = httpParser;
         this.logger = logger;
-        this.pipeline = pipeline;
     }
 
     `@Override`
     public Runnable create(Socket socket, Pipeline pipeline) {
         return new ConnectionHandler(socket, httpParser, logger, pipeline);
     }
 }

Update App.java line 59 accordingly: new DefaultConnectionHandlerFactory(httpParser, logger).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class DefaultConnectionHandlerFactory implements ConnectionHandlerFactory{
private final HttpParser httpParser;
private final Logger logger;
private final Pipeline pipeline;
public DefaultConnectionHandlerFactory(HttpParser httpParser, Logger logger, Pipeline pipeline) {
this.httpParser = httpParser;
this.logger = logger;
this.pipeline = pipeline;
}
@Override
public Runnable create(Socket socket, Pipeline pipeline) {
return new ConnectionHandler(socket, httpParser, logger, pipeline);
}
public class DefaultConnectionHandlerFactory implements ConnectionHandlerFactory{
private final HttpParser httpParser;
private final Logger logger;
public DefaultConnectionHandlerFactory(HttpParser httpParser, Logger logger) {
this.httpParser = httpParser;
this.logger = logger;
}
`@Override`
public Runnable create(Socket socket, Pipeline pipeline) {
return new ConnectionHandler(socket, httpParser, logger, pipeline);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/DefaultConnectionHandlerFactory.java` around lines 8
- 22, Remove the unused Pipeline field and constructor parameter from
DefaultConnectionHandlerFactory: delete the private final Pipeline pipeline
field, remove the Pipeline parameter from the constructor signature and body so
the constructor only takes (HttpParser httpParser, Logger logger) and stores
those two fields, and ensure create(Socket socket, Pipeline pipeline) continues
to use its method parameter when instantiating ConnectionHandler; update call
sites (e.g., the instantiation in App.java) to call new
DefaultConnectionHandlerFactory(httpParser, logger).

Comment on lines +15 to +18
public IpFilter(Set<String> whitelist, Set<String> blacklist) {
this.whitelist = whitelist;
this.blacklist = blacklist;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Defensively copy whitelist/blacklist to prevent external mutation and concurrent-modification hazards.

The constructor stores live references to the caller-supplied Set instances. Two problems:

  1. Security correctness: the caller can add or remove IPs after construction, silently changing the filter's access-control rules.
  2. Thread safety: HashSet is not thread-safe. If the caller mutates the set from another thread while doFilter reads it, a ConcurrentModificationException can occur.
🛡️ Proposed fix – take immutable snapshots at construction time
 public IpFilter(Set<String> whitelist, Set<String> blacklist) {
-    this.whitelist = whitelist;
-    this.blacklist = blacklist;
+    this.whitelist = whitelist != null ? Set.copyOf(whitelist) : null;
+    this.blacklist = blacklist != null ? Set.copyOf(blacklist) : null;
 }

Set.copyOf() (Java 10+) creates an immutable, null-element-rejecting snapshot that is safe for concurrent reads with no synchronization overhead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public IpFilter(Set<String> whitelist, Set<String> blacklist) {
this.whitelist = whitelist;
this.blacklist = blacklist;
}
public IpFilter(Set<String> whitelist, Set<String> blacklist) {
this.whitelist = whitelist != null ? Set.copyOf(whitelist) : null;
this.blacklist = blacklist != null ? Set.copyOf(blacklist) : null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/IpFilter.java` around lines 15 - 18, The
constructor for IpFilter currently stores caller-supplied Set references
(whitelist/blacklist), enabling external mutation and possible
ConcurrentModificationExceptions in doFilter; change the IpFilter(Set<String>
whitelist, Set<String> blacklist) constructor to defensively copy both inputs
into immutable snapshot sets (e.g., use Set.copyOf(...) or new HashSet<>(...)
wrapped/converted to an unmodifiable set) and assign those copies to the class
fields so doFilter reads a stable, thread-safe snapshot rather than live mutable
collections.

Comment on lines +151 to +179
private static HttpResponse createNotFoundResponse(String path) {
String html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 100px auto;
text-align: center;
}
h1 { color: #e74c3c; }
p { color: #666; }
</style>
</head>
<body>
<h1>404 - Not Found</h1>
<p>The requested resource <code>%s</code> was not found on this server.</p>
</body>
</html>
""".formatted(path);

Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "text/html; charset=utf-8");

return new HttpResponse(404, "Not Found", headers, html.getBytes(StandardCharsets.UTF_8));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

XSS: user-supplied path is embedded into HTML without escaping.

A request to /<script>alert(document.cookie)</script> passes isPathSafe (starts with /, no ..////\) and is injected verbatim into the <code> element in the 404 response. Browsers will execute the injected script.

🛡️ Proposed fix — HTML-escape the path before embedding
 private static HttpResponse createNotFoundResponse(String path) {
+    String safePath = path
+        .replace("&", "&amp;")
+        .replace("<", "&lt;")
+        .replace(">", "&gt;")
+        .replace("\"", "&quot;")
+        .replace("'", "&#x27;");
+
     String html = """
             ...
             <p>The requested resource <code>%s</code> was not found on this server.</p>
             ...
-            """.formatted(path);
+            """.formatted(safePath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/handler/StaticFileHandler.java` around lines 151 -
179, createNotFoundResponse embeds the user-supplied path into HTML without
escaping, allowing XSS; fix by HTML-escaping the path before formatting it into
the response (e.g. add or reuse a helper like escapeHtml(String) in
StaticFileHandler that replaces &, <, >, ", ' and forward-slash as needed) and
use the escaped value instead of the raw path when building the HTML string and
HttpResponse; keep the Content-Type header and UTF-8 byte conversion unchanged
and ensure the helper is invoked where createNotFoundResponse formats the HTML.

Comment on lines +50 to +62
byte[] body = new byte[0];
if (headers.containsKey(CONTENT_LENGTH)) {
int length;
try {
length = Integer.parseInt(headers.get(CONTENT_LENGTH));
} catch (NumberFormatException e) {
throw new IOException("Invalid Content-Length: " + headers.get(CONTENT_LENGTH), e);
}
if (length < 0) {
throw new IOException("Negative Content-Length: " + length);
}
body = in.readNBytes(length);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No upper bound on Content-Length — potential OOM/DoS vector.

A malicious client can send Content-Length: 2147483647 and readNBytes will attempt to allocate a ~2GB byte array, causing OutOfMemoryError and taking down the server. Add a maximum body size check.

Proposed fix
+    private static final int MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
+
     byte[] body = new byte[0];
     if (headers.containsKey(CONTENT_LENGTH)) {
         int length;
         try {
             length = Integer.parseInt(headers.get(CONTENT_LENGTH));
         } catch (NumberFormatException e) {
             throw new IOException("Invalid Content-Length: " + headers.get(CONTENT_LENGTH), e);
         }
         if (length < 0) {
             throw new IOException("Negative Content-Length: " + length);
         }
+        if (length > MAX_BODY_SIZE) {
+            throw new IOException("Content-Length exceeds maximum allowed size: " + length);
+        }
         body = in.readNBytes(length);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/http/HttpParser.java` around lines 50 - 62, The
Content-Length parsing in HttpParser currently allocates body =
in.readNBytes(length) without any upper bound; add a max allowed body size
constant (e.g., MAX_BODY_SIZE) and after parsing length in the block that
handles CONTENT_LENGTH verify length <= MAX_BODY_SIZE (and keep the existing
negative check), otherwise throw an IOException indicating the body is too
large; only call in.readNBytes(length) when the length is within bounds so the
server cannot OOM or be DOSed by large Content-Length values.

Comment on lines +27 to +38
String levelName = System.getProperty(
"log.level",
System.getenv().getOrDefault("LOG_LEVEL", ConfigLoader.getInstance().getLogLevel())
);

try {
Level level = Level.parse(levelName.toUpperCase());
logger.setLevel(level);
} catch (IllegalArgumentException e) {
logger.setLevel(Level.INFO);
logger.warning("Invalid log level: '" + levelName + "', defaulting to INFO");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

NPE crashes server startup when ConfigLoader.getLogLevel() returns null.

ConfigLoader.logLevel is only assigned inside if (loggingConfig != null), so it returns null when the logging section is absent from application-properties.yml. The call chain System.getenv().getOrDefault("LOG_LEVEL", null) then System.getProperty("log.level", null) can both propagate that null, and null.toUpperCase() on line 33 throws a NullPointerException. The catch block only handles IllegalArgumentException, so the NPE propagates as an ExceptionInInitializerError, preventing the server from starting.

🛡️ Proposed fix
         String levelName = System.getProperty(
             "log.level",
-            System.getenv().getOrDefault("LOG_LEVEL", ConfigLoader.getInstance().getLogLevel())
+            System.getenv().getOrDefault("LOG_LEVEL",
+                Optional.ofNullable(ConfigLoader.getInstance().getLogLevel()).orElse("INFO"))
         );

Add import java.util.Optional; at the top of the file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String levelName = System.getProperty(
"log.level",
System.getenv().getOrDefault("LOG_LEVEL", ConfigLoader.getInstance().getLogLevel())
);
try {
Level level = Level.parse(levelName.toUpperCase());
logger.setLevel(level);
} catch (IllegalArgumentException e) {
logger.setLevel(Level.INFO);
logger.warning("Invalid log level: '" + levelName + "', defaulting to INFO");
}
String levelName = System.getProperty(
"log.level",
System.getenv().getOrDefault("LOG_LEVEL",
Optional.ofNullable(ConfigLoader.getInstance().getLogLevel()).orElse("INFO"))
);
try {
Level level = Level.parse(levelName.toUpperCase());
logger.setLevel(level);
} catch (IllegalArgumentException e) {
logger.setLevel(Level.INFO);
logger.warning("Invalid log level: '" + levelName + "', defaulting to INFO");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/logging/ServerLogging.java` around lines 27 - 38,
levelName can be null (from ConfigLoader.getLogLevel()) which leads to a
NullPointerException when calling toUpperCase(); change the logic that computes
levelName (the variable used by Level.parse in ServerLogging) to provide a
non-null fallback (e.g., "INFO") before calling toUpperCase()/Level.parse, or
wrap the value with Optional/ternary check and only call toUpperCase() when
non-null; ensure the try/catch still handles IllegalArgumentException from
Level.parse and that logger.setLevel is called with the fallback Level.INFO when
input is missing or invalid.

Comment on lines +62 to +64
} catch (Exception e) {
throw new RuntimeException("Failed to load application config");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exception cause is swallowed, and the file-not-found IllegalArgumentException message is silently replaced.

Two related problems:

  1. throw new RuntimeException("Failed to load application config") drops the root-cause exception e. When a ClassCastException or IOException occurs during YAML parsing, the stack trace is completely lost, making debugging very hard.

  2. The IllegalArgumentException thrown at line 33 ("Did not find application-properties.yml") is thrown inside the try block, so catch (Exception e) catches it, and the specific diagnostic message is replaced by the generic "Failed to load application config" string.

🐛 Proposed fix
-        } catch (Exception e) {
-            throw new RuntimeException("Failed to load application config");
-        }
+        } catch (IllegalArgumentException e) {
+            throw e;   // re-throw "Did not find application-properties.yml" as-is
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to load application config", e);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (Exception e) {
throw new RuntimeException("Failed to load application config");
}
} catch (IllegalArgumentException e) {
throw e; // re-throw "Did not find application-properties.yml" as-is
} catch (Exception e) {
throw new RuntimeException("Failed to load application config", e);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/util/ConfigLoader.java` around lines 62 - 64, In
ConfigLoader, preserve the original exception and avoid swallowing the specific
IllegalArgumentException from the missing file check: update the catch(Exception
e) in the config-loading method to rethrow a RuntimeException that includes e as
the cause (e.g., new RuntimeException("Failed to load application config", e)),
and ensure the earlier IllegalArgumentException thrown with message "Did not
find application-properties.yml" is not captured and replaced (either move that
file-existence check outside the try block or rethrow IllegalArgumentException
immediately if caught).

Comment on lines +16 to +18
window.addEventListener("popstate", () => {
navigate(window.location.pathname);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

popstatenavigatepushState creates duplicate history entries, breaking back/forward navigation.

When the user presses Back, popstate fires and calls navigate(), which calls history.pushState() on line 54. This pushes a new entry instead of restoring the old one, making the back button effectively non-functional (infinite loop between two states).

Use history.replaceState in the popstate path, or split the logic so popstate doesn't push:

Proposed fix
 window.addEventListener("popstate", () => {
-    navigate(window.location.pathname);
+    navigateTo(window.location.pathname, false);
 });

-function navigate(href) {
+function navigate(href) {
+    navigateTo(href, true);
+}
+
+function navigateTo(href, pushHistory) {
     nav.classList.add("disable-anchors");
     const main = document.getElementById("main-content");

     main.classList.add("fade-out");

     setTimeout(() => {
         fetch(href)
             .then(res => {
                 if (!res.ok) throw new Error(`HTTP ${res.status}`);
                 return  res.text();
             })
             .then(html => {
                 const doc = new DOMParser().parseFromString(html, "text/html");
                 const newMain = doc.querySelector("main");

                 if (!newMain) throw new Error("No <main> found in " + href);

                 main.innerHTML = newMain.innerHTML;
-                history.pushState(null, "", href);
+                if (pushHistory) history.pushState(null, "", href);
                 route(href);

                 main.classList.remove("fade-out");

                 setTimeout(() => {
                     nav.classList.remove("disable-anchors");
                 }, 150);
             })
             .catch(err => {
                 console.error("Navigation failed:", err);
                 main.classList.remove("fade-out");
                 nav.classList.remove("disable-anchors");
             });
     }, 200);
 }

Also applies to: 54-54

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/js/app.js` around lines 16 - 18, The popstate
handler calls navigate(window.location.pathname) which then calls
history.pushState (in navigate) and creates duplicate history entries; modify
the logic so that when navigate is invoked from the popstate handler it does not
call pushState—either call history.replaceState inside the popstate path or add
a flag/parameter to navigate (e.g., navigate(path, {fromPopState: true})) and
have navigate skip pushState and use replaceState when that flag is set,
ensuring pushState remains for user-initiated navigations only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.